﻿/*--------------------------------------------------------------------------
 * linq.js - LINQ for JavaScript
 * licensed under MIT License
 *------------------------------------------------------------------------*/

(function (root, factory) {
    'use strict';
    if (typeof define === 'function' && define['amd']) {
        define(factory);
    } else if (typeof exports === 'object') {
        module['exports'] = factory();
    } else {
        root['SAT'] = factory();
    }
})(this, function () {
    var isFlash = typeof fl !== 'undefined';

    var Functions = {
        Identity: function (x) {
            return x;
        },
        True: function () {
            return true;
        },
        Blank: function () {}
    };

    var Types = {
        Boolean: typeof true,
        Number: typeof 0,
        String: typeof '',
        Object: typeof {},
        Undefined: typeof undefined,
        Function: typeof function () {}
    };

    var funcCache = { '': Functions.Identity };

    var Utils = {
        createLambda: function (expression) {
            if (expression == null) return Functions.Identity;
            if (typeof expression === Types.String) {
                // get from cache
                var f = funcCache[expression];
                if (f != null) {
                    return f;
                }

                if (expression.indexOf('=>') === -1) {
                    const regexp = new RegExp('[$]+', 'g');

                    var maxLength = 0;
                    var match;
                    while ((match = regexp.exec(expression)) != null) {
                        if (match[0].length > maxLength) {
                            maxLength = match[0].length;
                        }
                    }

                    const argArray = [];
                    for (var i = 1; i <= maxLength; i++) {
                        var dollar = '';
                        for (var j = 0; j < i; j++) {
                            dollar += '$';
                        }
                        argArray.push(dollar);
                    }

                    const args = argArray.join(',');

                    f = new Function(args, 'return ' + expression);
                    funcCache[expression] = f;
                    return f;
                } else {
                    const expr = expression.match(/^[(\s]*([^()]*?)[)\s]*=>(.*)/);
                    f = new Function(
                        expr[1],
                        expr[2].match(/\breturn\b/) ? expr[2] : 'return ' + expr[2]
                    );
                    funcCache[expression] = f;
                    return f;
                }
            }
            return expression;
        },

        defineProperty: function (target, methodName, value) {
            Object.defineProperty(target, methodName, {
                enumerable: false,
                configurable: true,
                writable: true,
                value: value
            });
        },

        compare: function (a, b) {
            return a === b ? 0 : a > b ? 1 : -1;
        },

        dispose: function (obj) {
            if (obj != null) obj.dispose();
        },

        hasNativeIteratorSupport: function () {
            return (
                typeof Symbol !== 'undefined' && typeof Symbol.iterator !== 'undefined'
            );
        }
    };

    var State = { Before: 0, Running: 1, After: 2 };

    var IEnumerator = function (initialize, tryGetNext, dispose) {
        var yielder = new Yielder();
        var state = State.Before;

        this.current = yielder.current;

        this.moveNext = function () {
            try {
                switch (state) {
                    case State.Before:
                        state = State.Running;
                        initialize();
                    // fall through

                    case State.Running:
                        if (tryGetNext.apply(yielder)) {
                            return true;
                        } else {
                            this.dispose();
                            return false;
                        }
                    // fall through

                    case State.After:
                        return false;
                }
            } catch (e) {
                this.dispose();
                throw e;
            }
        };

        this.dispose = function () {
            if (state != State.Running) return;

            try {
                dispose();
            } finally {
                state = State.After;
            }
        };
    };

    // tryGetNext yielder
    var Yielder = function () {
        var current = null;
        this.current = function () {
            return current;
        };
        this.yieldReturn = function (value) {
            current = value;
            return true;
        };
        this.yieldBreak = function () {
            return false;
        };
    };

    // Enumerable constuctor
    var Enumerable = function (getEnumerator) {
        this.getEnumerator = getEnumerator;
    };

    ///////////////////
    // Utility Methods

    Enumerable.Utils = {};

    Enumerable.Utils.createLambda = function (expression) {
        return Utils.createLambda(expression);
    };

    Enumerable.Utils.createEnumerable = function (getEnumerator) {
        return new Enumerable(getEnumerator);
    };

    Enumerable.Utils.createEnumerator = function (initialize, tryGetNext, dispose) {
        return new IEnumerator(initialize, tryGetNext, dispose);
    };

    Enumerable.Utils.extendTo = function (type) {
        var typeProto = type.prototype;
        var enumerableProto;

        if (type === Array) {
            enumerableProto = ArrayEnumerable.prototype;
            Utils.defineProperty(typeProto, 'getSource', function () {
                return this;
            });
        } else {
            enumerableProto = Enumerable.prototype;
            Utils.defineProperty(typeProto, 'getEnumerator', function () {
                return Enumerable.from(this).getEnumerator();
            });
        }

        for (var methodName in enumerableProto) {
            const func = enumerableProto[methodName];

            // already extended
            if (typeProto[methodName] == func) continue;

            // already defined(example Array#reverse/join/forEach...)
            if (typeProto[methodName] != null) {
                methodName = methodName + 'ByLinq';
                if (typeProto[methodName] == func) continue; // recheck
            }

            if (func instanceof Function) {
                Utils.defineProperty(typeProto, methodName, func);
            }
        }
    };

    Enumerable.Utils.recallFrom = function (type) {
        var typeProto = type.prototype;
        var enumerableProto;

        if (type === Array) {
            enumerableProto = ArrayEnumerable.prototype;
            delete typeProto.getSource;
        } else {
            enumerableProto = Enumerable.prototype;
            delete typeProto.getEnumerator;
        }

        for (var methodName in enumerableProto) {
            const func = enumerableProto[methodName];

            if (typeProto[methodName + 'ByLinq']) {
                delete typeProto[methodName + 'ByLinq'];
            } else if (typeProto[methodName] == func && func instanceof Function) {
                delete typeProto[methodName];
            }
        }
    };

    //////////////
    // Generators

    Enumerable.choice = function () {
        var args = arguments;

        return new Enumerable(function () {
            return new IEnumerator(
                function () {
                    args =
                        args[0] instanceof Array
                            ? args[0]
                            : args[0].getEnumerator != null
                              ? args[0].toArray()
                              : args;
                },
                function () {
                    return this.yieldReturn(
                        args[Math.floor(Math.random() * args.length)]
                    );
                },
                Functions.Blank
            );
        });
    };

    Enumerable.cycle = function () {
        var args = arguments;

        return new Enumerable(function () {
            var index = 0;
            return new IEnumerator(
                function () {
                    args =
                        args[0] instanceof Array
                            ? args[0]
                            : args[0].getEnumerator != null
                              ? args[0].toArray()
                              : args;
                },
                function () {
                    if (index >= args.length) index = 0;
                    return this.yieldReturn(args[index++]);
                },
                Functions.Blank
            );
        });
    };

    Enumerable.empty = function () {
        return new Enumerable(function () {
            return new IEnumerator(
                Functions.Blank,
                function () {
                    return false;
                },
                Functions.Blank
            );
        });
    };

    (Enumerable.from = function (obj) {
        if (obj == null) {
            return Enumerable.empty();
        }
        if (obj instanceof Enumerable) {
            return obj;
        }
        if (typeof obj == Types.Number || typeof obj == Types.Boolean) {
            return Enumerable.repeat(obj, 1);
        }
        if (typeof obj == Types.String) {
            return new Enumerable(function () {
                var index = 0;
                return new IEnumerator(
                    Functions.Blank,
                    function () {
                        return index < obj.length
                            ? this.yieldReturn(obj.charAt(index++))
                            : false;
                    },
                    Functions.Blank
                );
            });
        }
        if (typeof obj == Types.Function && Object.keys(obj).length == 0) {
            return new Enumerable(function () {
                var orig;

                return new IEnumerator(
                    function () {
                        orig = obj()[Symbol.iterator]();
                    },
                    function () {
                        var next = orig.next();
                        return next.done ? false : this.yieldReturn(next.value);
                    },
                    Functions.Blank
                );
            });
        }

        if (typeof obj != Types.Function) {
            // array or array-like object
            if (typeof obj.length == Types.Number) {
                return new ArrayEnumerable(obj);
            }

            // iterable object
            if (
                typeof Symbol !== 'undefined' &&
                typeof obj[Symbol.iterator] !== 'undefined'
            ) {
                var iterator;
                return new Enumerable(function () {
                    return new IEnumerator(
                        function () {
                            iterator = obj[Symbol.iterator]();
                        },
                        function () {
                            var next = iterator.next();
                            return next.done ? false : this.yieldReturn(next.value);
                        },
                        Functions.Blank
                    );
                });
            }

            // object conforming to the iterator protocol
            if (typeof obj.next == Types.Function) {
                return new Enumerable(function () {
                    return new IEnumerator(
                        Functions.Blank,
                        function () {
                            var next = obj.next();
                            return next.done ? false : this.yieldReturn(next.value);
                        },
                        Functions.Blank
                    );
                });
            }
        }

        // case function/object: create keyValuePair[]
        return new Enumerable(function () {
            var array = [];
            var index = 0;

            return new IEnumerator(
                function () {
                    for (var key in obj) {
                        const value = obj[key];
                        if (
                            !(value instanceof Function) &&
                            Object.prototype.hasOwnProperty.call(obj, key)
                        ) {
                            array.push({ key: key, value: value });
                        }
                    }
                },
                function () {
                    return index < array.length
                        ? this.yieldReturn(array[index++])
                        : false;
                },
                Functions.Blank
            );
        });
    }),
        (Enumerable.make = function (element) {
            return Enumerable.repeat(element, 1);
        });

    // Overload:function(input, pattern)
    // Overload:function(input, pattern, flags)
    Enumerable.matches = function (input, pattern, flags) {
        if (flags == null) flags = '';

        if (pattern instanceof RegExp) {
            flags += pattern.ignoreCase ? 'i' : '';
            flags += pattern.multiline ? 'm' : '';
            pattern = pattern.source;
        }
        if (flags.indexOf('g') === -1) flags += 'g';

        return new Enumerable(function () {
            var regex;
            return new IEnumerator(
                function () {
                    regex = new RegExp(pattern, flags);
                },
                function () {
                    var match = regex.exec(input);
                    return match ? this.yieldReturn(match) : false;
                },
                Functions.Blank
            );
        });
    };

    // Overload:function(start, count)
    // Overload:function(start, count, step)
    Enumerable.range = function (start, count, step) {
        if (step == null) step = 1;

        return new Enumerable(function () {
            var value;
            var index = 0;

            return new IEnumerator(
                function () {
                    value = start - step;
                },
                function () {
                    return index++ < count
                        ? this.yieldReturn((value += step))
                        : this.yieldBreak();
                },
                Functions.Blank
            );
        });
    };

    // Overload:function(start, count)
    // Overload:function(start, count, step)
    Enumerable.rangeDown = function (start, count, step) {
        if (step == null) step = 1;

        return new Enumerable(function () {
            var value;
            var index = 0;

            return new IEnumerator(
                function () {
                    value = start + step;
                },
                function () {
                    return index++ < count
                        ? this.yieldReturn((value -= step))
                        : this.yieldBreak();
                },
                Functions.Blank
            );
        });
    };

    // Overload:function(start, to)
    // Overload:function(start, to, step)
    Enumerable.rangeTo = function (start, to, step) {
        if (step == null) step = 1;

        if (start < to) {
            return new Enumerable(function () {
                var value;

                return new IEnumerator(
                    function () {
                        value = start - step;
                    },
                    function () {
                        var next = (value += step);
                        return next <= to ? this.yieldReturn(next) : this.yieldBreak();
                    },
                    Functions.Blank
                );
            });
        } else {
            return new Enumerable(function () {
                var value;

                return new IEnumerator(
                    function () {
                        value = start + step;
                    },
                    function () {
                        var next = (value -= step);
                        return next >= to ? this.yieldReturn(next) : this.yieldBreak();
                    },
                    Functions.Blank
                );
            });
        }
    };

    // Overload:function(element)
    // Overload:function(element, count)
    Enumerable.repeat = function (element, count) {
        if (count != null) return Enumerable.repeat(element).take(count);

        return new Enumerable(function () {
            return new IEnumerator(
                Functions.Blank,
                function () {
                    return this.yieldReturn(element);
                },
                Functions.Blank
            );
        });
    };

    Enumerable.repeatWithFinalize = function (initializer, finalizer) {
        initializer = Utils.createLambda(initializer);
        finalizer = Utils.createLambda(finalizer);

        return new Enumerable(function () {
            var element;
            return new IEnumerator(
                function () {
                    element = initializer();
                },
                function () {
                    return this.yieldReturn(element);
                },
                function () {
                    if (element != null) {
                        finalizer(element);
                        element = null;
                    }
                }
            );
        });
    };

    // Overload:function(func)
    // Overload:function(func, count)
    Enumerable.generate = function (func, count) {
        if (count != null) return Enumerable.generate(func).take(count);

        func = Utils.createLambda(func);

        return new Enumerable(function () {
            return new IEnumerator(
                Functions.Blank,
                function () {
                    return this.yieldReturn(func());
                },
                Functions.Blank
            );
        });
    };

    // Overload:function()
    // Overload:function(start)
    // Overload:function(start, step)
    Enumerable.toInfinity = function (start, step) {
        if (start == null) start = 0;
        if (step == null) step = 1;

        return new Enumerable(function () {
            var value;
            return new IEnumerator(
                function () {
                    value = start - step;
                },
                function () {
                    return this.yieldReturn((value += step));
                },
                Functions.Blank
            );
        });
    };

    // Overload:function()
    // Overload:function(start)
    // Overload:function(start, step)
    Enumerable.toNegativeInfinity = function (start, step) {
        if (start == null) start = 0;
        if (step == null) step = 1;

        return new Enumerable(function () {
            var value;
            return new IEnumerator(
                function () {
                    value = start + step;
                },
                function () {
                    return this.yieldReturn((value -= step));
                },
                Functions.Blank
            );
        });
    };

    Enumerable.unfold = function (seed, func) {
        func = Utils.createLambda(func);

        return new Enumerable(function () {
            var isFirst = true;
            var value;
            return new IEnumerator(
                Functions.Blank,
                function () {
                    if (isFirst) {
                        isFirst = false;
                        value = seed;
                        return this.yieldReturn(value);
                    }
                    value = func(value);
                    return this.yieldReturn(value);
                },
                Functions.Blank
            );
        });
    };

    Enumerable.defer = function (enumerableFactory) {
        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = Enumerable.from(enumerableFactory()).getEnumerator();
                },
                function () {
                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : this.yieldBreak();
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    /////////////////////
    // Extension Methods

    ////////////////////////////////////
    // Projection and Filtering Methods

    // Overload:function(func)
    // Overload:function(func, resultSelector<element>)
    // Overload:function(func, resultSelector<element, nestLevel>)
    Enumerable.prototype.traverseBreadthFirst = function (func, resultSelector) {
        var source = this;
        func = Utils.createLambda(func);
        resultSelector = Utils.createLambda(resultSelector);

        return new Enumerable(function () {
            var enumerator;
            var nestLevel = 0;
            var buffer = [];

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (true) {
                        if (enumerator.moveNext()) {
                            buffer.push(enumerator.current());
                            return this.yieldReturn(
                                resultSelector(enumerator.current(), nestLevel)
                            );
                        }

                        const next = Enumerable.from(buffer).selectMany(function (x) {
                            return func(x);
                        });
                        if (!next.any()) {
                            return false;
                        } else {
                            nestLevel++;
                            buffer = [];
                            Utils.dispose(enumerator);
                            enumerator = next.getEnumerator();
                        }
                    }
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(func)
    // Overload:function(func, resultSelector<element>)
    // Overload:function(func, resultSelector<element, nestLevel>)
    Enumerable.prototype.traverseDepthFirst = function (func, resultSelector) {
        var source = this;
        func = Utils.createLambda(func);
        resultSelector = Utils.createLambda(resultSelector);

        return new Enumerable(function () {
            var enumeratorStack = [];
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (true) {
                        if (enumerator.moveNext()) {
                            const value = resultSelector(
                                enumerator.current(),
                                enumeratorStack.length
                            );
                            enumeratorStack.push(enumerator);
                            enumerator = Enumerable.from(
                                func(enumerator.current())
                            ).getEnumerator();
                            return this.yieldReturn(value);
                        }

                        if (enumeratorStack.length <= 0) return false;
                        Utils.dispose(enumerator);
                        enumerator = enumeratorStack.pop();
                    }
                },
                function () {
                    try {
                        Utils.dispose(enumerator);
                    } finally {
                        Enumerable.from(enumeratorStack).forEach(function (s) {
                            s.dispose();
                        });
                    }
                }
            );
        });
    };

    Enumerable.prototype.flatten = function () {
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var middleEnumerator = null;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (true) {
                        if (middleEnumerator != null) {
                            if (middleEnumerator.moveNext()) {
                                return this.yieldReturn(middleEnumerator.current());
                            } else {
                                middleEnumerator = null;
                            }
                        }

                        if (enumerator.moveNext()) {
                            if (enumerator.current() instanceof Array) {
                                Utils.dispose(middleEnumerator);
                                middleEnumerator = Enumerable.from(enumerator.current())
                                    .selectMany(Functions.Identity)
                                    .flatten()
                                    .getEnumerator();
                                continue;
                            } else {
                                return this.yieldReturn(enumerator.current());
                            }
                        }

                        return false;
                    }
                },
                function () {
                    try {
                        Utils.dispose(enumerator);
                    } finally {
                        Utils.dispose(middleEnumerator);
                    }
                }
            );
        });
    };

    Enumerable.prototype.pairwise = function (selector) {
        var source = this;
        selector = Utils.createLambda(selector);

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                    enumerator.moveNext();
                },
                function () {
                    var prev = enumerator.current();
                    return enumerator.moveNext()
                        ? this.yieldReturn(selector(prev, enumerator.current()))
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(func)
    // Overload:function(seed,func<value,element>)
    Enumerable.prototype.scan = function (seed, func) {
        var isUseSeed;
        if (func == null) {
            func = Utils.createLambda(seed);
            isUseSeed = false;
        } else {
            func = Utils.createLambda(func);
            isUseSeed = true;
        }
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var value;
            var isFirst = true;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    if (isFirst) {
                        isFirst = false;
                        if (!isUseSeed) {
                            if (enumerator.moveNext()) {
                                return this.yieldReturn((value = enumerator.current()));
                            }
                        } else {
                            return this.yieldReturn((value = seed));
                        }
                    }

                    return enumerator.moveNext()
                        ? this.yieldReturn((value = func(value, enumerator.current())))
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(selector<element>)
    // Overload:function(selector<element,index>)
    Enumerable.prototype.select = function (selector) {
        selector = Utils.createLambda(selector);

        if (selector.length <= 1) {
            return new WhereSelectEnumerable(this, null, selector);
        } else {
            var source = this;

            return new Enumerable(function () {
                var enumerator;
                var index = 0;

                return new IEnumerator(
                    function () {
                        enumerator = source.getEnumerator();
                    },
                    function () {
                        return enumerator.moveNext()
                            ? this.yieldReturn(selector(enumerator.current(), index++))
                            : false;
                    },
                    function () {
                        Utils.dispose(enumerator);
                    }
                );
            });
        }
    };

    // Overload:function(collectionSelector<element>)
    // Overload:function(collectionSelector<element,index>)
    // Overload:function(collectionSelector<element>,resultSelector)
    // Overload:function(collectionSelector<element,index>,resultSelector)
    Enumerable.prototype.selectMany = function (collectionSelector, resultSelector) {
        var source = this;
        collectionSelector = Utils.createLambda(collectionSelector);
        if (resultSelector == null)
            resultSelector = function (a, b) {
                return b;
            };
        resultSelector = Utils.createLambda(resultSelector);

        return new Enumerable(function () {
            var enumerator;
            var middleEnumerator = undefined;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    if (middleEnumerator === undefined) {
                        if (!enumerator.moveNext()) return false;
                    }
                    do {
                        if (middleEnumerator == null) {
                            const middleSeq = collectionSelector(
                                enumerator.current(),
                                index++
                            );
                            middleEnumerator = Enumerable.from(middleSeq).getEnumerator();
                        }
                        if (middleEnumerator.moveNext()) {
                            return this.yieldReturn(
                                resultSelector(
                                    enumerator.current(),
                                    middleEnumerator.current()
                                )
                            );
                        }
                        Utils.dispose(middleEnumerator);
                        middleEnumerator = null;
                    } while (enumerator.moveNext());
                    return false;
                },
                function () {
                    try {
                        Utils.dispose(enumerator);
                    } finally {
                        Utils.dispose(middleEnumerator);
                    }
                }
            );
        });
    };

    // Overload:function(predicate<element>)
    // Overload:function(predicate<element,index>)
    Enumerable.prototype.where = function (predicate) {
        predicate = Utils.createLambda(predicate);

        if (predicate.length <= 1) {
            return new WhereEnumerable(this, predicate);
        } else {
            var source = this;

            return new Enumerable(function () {
                var enumerator;
                var index = 0;

                return new IEnumerator(
                    function () {
                        enumerator = source.getEnumerator();
                    },
                    function () {
                        while (enumerator.moveNext()) {
                            if (predicate(enumerator.current(), index++)) {
                                return this.yieldReturn(enumerator.current());
                            }
                        }
                        return false;
                    },
                    function () {
                        Utils.dispose(enumerator);
                    }
                );
            });
        }
    };

    // Overload:function(selector<element>)
    // Overload:function(selector<element,index>)
    Enumerable.prototype.choose = function (selector) {
        selector = Utils.createLambda(selector);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (enumerator.moveNext()) {
                        const result = selector(enumerator.current(), index++);
                        if (result != null) {
                            return this.yieldReturn(result);
                        }
                    }
                    return this.yieldBreak();
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.ofType = function (type) {
        var typeName;
        switch (type) {
            case Number:
                typeName = Types.Number;
                break;
            case String:
                typeName = Types.String;
                break;
            case Boolean:
                typeName = Types.Boolean;
                break;
            case Function:
                typeName = Types.Function;
                break;
            default:
                typeName = null;
                break;
        }
        return typeName === null
            ? this.where(function (x) {
                  return x instanceof type;
              })
            : this.where(function (x) {
                  return typeof x === typeName;
              });
    };

    // mutiple arguments, last one is selector, others are enumerable
    Enumerable.prototype.zip = function () {
        var args = arguments;
        var selector = Utils.createLambda(arguments[arguments.length - 1]);

        var source = this;
        // optimized case:argument is 2
        if (arguments.length == 2) {
            const second = arguments[0];

            return new Enumerable(function () {
                var firstEnumerator;
                var secondEnumerator;
                var index = 0;

                return new IEnumerator(
                    function () {
                        firstEnumerator = source.getEnumerator();
                        secondEnumerator = Enumerable.from(second).getEnumerator();
                    },
                    function () {
                        if (firstEnumerator.moveNext() && secondEnumerator.moveNext()) {
                            return this.yieldReturn(
                                selector(
                                    firstEnumerator.current(),
                                    secondEnumerator.current(),
                                    index++
                                )
                            );
                        }
                        return false;
                    },
                    function () {
                        try {
                            Utils.dispose(firstEnumerator);
                        } finally {
                            Utils.dispose(secondEnumerator);
                        }
                    }
                );
            });
        } else {
            return new Enumerable(function () {
                var enumerators;
                var index = 0;

                return new IEnumerator(
                    function () {
                        var array = Enumerable.make(source)
                            .concat(
                                Enumerable.from(args)
                                    .takeExceptLast()
                                    .select(Enumerable.from)
                            )
                            .select(function (x) {
                                return x.getEnumerator();
                            })
                            .toArray();
                        enumerators = Enumerable.from(array);
                    },
                    function () {
                        if (
                            enumerators.all(function (x) {
                                return x.moveNext();
                            })
                        ) {
                            const array = enumerators
                                .select(function (x) {
                                    return x.current();
                                })
                                .toArray();
                            array.push(index++);
                            return this.yieldReturn(selector.apply(null, array));
                        } else {
                            return this.yieldBreak();
                        }
                    },
                    function () {
                        Enumerable.from(enumerators).forEach(Utils.dispose);
                    }
                );
            });
        }
    };

    // mutiple arguments
    Enumerable.prototype.merge = function () {
        var args = arguments;
        var source = this;

        return new Enumerable(function () {
            var enumerators;
            var index = -1;

            return new IEnumerator(
                function () {
                    enumerators = Enumerable.make(source)
                        .concat(Enumerable.from(args).select(Enumerable.from))
                        .select(function (x) {
                            return x.getEnumerator();
                        })
                        .toArray();
                },
                function () {
                    while (enumerators.length > 0) {
                        index = index >= enumerators.length - 1 ? 0 : index + 1;
                        const enumerator = enumerators[index];

                        if (enumerator.moveNext()) {
                            return this.yieldReturn(enumerator.current());
                        } else {
                            enumerator.dispose();
                            enumerators.splice(index--, 1);
                        }
                    }
                    return this.yieldBreak();
                },
                function () {
                    Enumerable.from(enumerators).forEach(Utils.dispose);
                }
            );
        });
    };

    ////////////////
    // Join Methods

    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
    Enumerable.prototype.join = function (
        inner,
        outerKeySelector,
        innerKeySelector,
        resultSelector,
        compareSelector
    ) {
        outerKeySelector = Utils.createLambda(outerKeySelector);
        innerKeySelector = Utils.createLambda(innerKeySelector);
        resultSelector = Utils.createLambda(resultSelector);
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var outerEnumerator;
            var lookup;
            var innerElements = null;
            var innerCount = 0;

            return new IEnumerator(
                function () {
                    outerEnumerator = source.getEnumerator();
                    lookup = Enumerable.from(inner).toLookup(
                        innerKeySelector,
                        Functions.Identity,
                        compareSelector
                    );
                },
                function () {
                    while (true) {
                        if (innerElements != null) {
                            var innerElement = innerElements[innerCount++];
                            if (innerElement !== undefined) {
                                return this.yieldReturn(
                                    resultSelector(
                                        outerEnumerator.current(),
                                        innerElement
                                    )
                                );
                            }

                            innerElement = null;
                            innerCount = 0;
                        }

                        if (outerEnumerator.moveNext()) {
                            const key = outerKeySelector(outerEnumerator.current());
                            innerElements = lookup.get(key).toArray();
                        } else {
                            return false;
                        }
                    }
                },
                function () {
                    Utils.dispose(outerEnumerator);
                }
            );
        });
    };

    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
    Enumerable.prototype.leftJoin = function (
        inner,
        outerKeySelector,
        innerKeySelector,
        resultSelector,
        compareSelector
    ) {
        outerKeySelector = Utils.createLambda(outerKeySelector);
        innerKeySelector = Utils.createLambda(innerKeySelector);
        resultSelector = Utils.createLambda(resultSelector);
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var outerEnumerator;
            var lookup;
            var innerElements = null;
            var innerCount = 0;

            return new IEnumerator(
                function () {
                    outerEnumerator = source.getEnumerator();
                    lookup = Enumerable.from(inner).toLookup(
                        innerKeySelector,
                        Functions.Identity,
                        compareSelector
                    );
                },
                function () {
                    while (true) {
                        if (innerElements != null) {
                            var innerElement = innerElements[innerCount++];
                            if (innerElement !== undefined) {
                                return this.yieldReturn(
                                    resultSelector(
                                        outerEnumerator.current(),
                                        innerElement
                                    )
                                );
                            }

                            innerElement = null;
                            innerCount = 0;
                        }

                        if (outerEnumerator.moveNext()) {
                            const key = outerKeySelector(outerEnumerator.current());
                            innerElements = lookup.get(key).toArray();
                            // execute once if innerElements is NULL
                            if (innerElements == null || innerElements.length == 0) {
                                return this.yieldReturn(
                                    resultSelector(outerEnumerator.current(), null)
                                );
                            }
                        } else {
                            return false;
                        }
                    }
                },
                function () {
                    Utils.dispose(outerEnumerator);
                }
            );
        });
    };

    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector)
    // Overload:function (inner, outerKeySelector, innerKeySelector, resultSelector, compareSelector)
    Enumerable.prototype.groupJoin = function (
        inner,
        outerKeySelector,
        innerKeySelector,
        resultSelector,
        compareSelector
    ) {
        outerKeySelector = Utils.createLambda(outerKeySelector);
        innerKeySelector = Utils.createLambda(innerKeySelector);
        resultSelector = Utils.createLambda(resultSelector);
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var enumerator = source.getEnumerator();
            var lookup = null;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                    lookup = Enumerable.from(inner).toLookup(
                        innerKeySelector,
                        Functions.Identity,
                        compareSelector
                    );
                },
                function () {
                    if (enumerator.moveNext()) {
                        const innerElement = lookup.get(
                            outerKeySelector(enumerator.current())
                        );
                        return this.yieldReturn(
                            resultSelector(enumerator.current(), innerElement)
                        );
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    ///////////////
    // Set Methods

    Enumerable.prototype.all = function (predicate) {
        predicate = Utils.createLambda(predicate);

        var result = true;
        this.forEach(function (x) {
            if (!predicate(x)) {
                result = false;
                return false; // break
            }
        });
        return result;
    };

    // Overload:function()
    // Overload:function(predicate)
    Enumerable.prototype.any = function (predicate) {
        predicate = Utils.createLambda(predicate);

        var enumerator = this.getEnumerator();
        try {
            if (arguments.length == 0) return enumerator.moveNext(); // case:function()

            while (enumerator.moveNext()) {
                // case:function(predicate)
                if (predicate(enumerator.current())) return true;
            }
            return false;
        } finally {
            Utils.dispose(enumerator);
        }
    };

    Enumerable.prototype.isEmpty = function () {
        return !this.any();
    };

    // multiple arguments
    Enumerable.prototype.concat = function () {
        var source = this;

        if (arguments.length == 1) {
            const second = arguments[0];

            return new Enumerable(function () {
                var firstEnumerator;
                var secondEnumerator;

                return new IEnumerator(
                    function () {
                        firstEnumerator = source.getEnumerator();
                    },
                    function () {
                        if (secondEnumerator == null) {
                            if (firstEnumerator.moveNext())
                                return this.yieldReturn(firstEnumerator.current());
                            secondEnumerator = Enumerable.from(second).getEnumerator();
                        }
                        if (secondEnumerator.moveNext())
                            return this.yieldReturn(secondEnumerator.current());
                        return false;
                    },
                    function () {
                        try {
                            Utils.dispose(firstEnumerator);
                        } finally {
                            Utils.dispose(secondEnumerator);
                        }
                    }
                );
            });
        } else {
            const args = arguments;

            return new Enumerable(function () {
                var enumerators;

                return new IEnumerator(
                    function () {
                        enumerators = Enumerable.make(source)
                            .concat(Enumerable.from(args).select(Enumerable.from))
                            .select(function (x) {
                                return x.getEnumerator();
                            })
                            .toArray();
                    },
                    function () {
                        while (enumerators.length > 0) {
                            const enumerator = enumerators[0];

                            if (enumerator.moveNext()) {
                                return this.yieldReturn(enumerator.current());
                            } else {
                                enumerator.dispose();
                                enumerators.splice(0, 1);
                            }
                        }
                        return this.yieldBreak();
                    },
                    function () {
                        Enumerable.from(enumerators).forEach(Utils.dispose);
                    }
                );
            });
        }
    };

    Enumerable.prototype.insert = function (index, second) {
        var source = this;

        return new Enumerable(function () {
            var firstEnumerator;
            var secondEnumerator;
            var count = 0;
            var isEnumerated = false;

            return new IEnumerator(
                function () {
                    firstEnumerator = source.getEnumerator();
                    secondEnumerator = Enumerable.from(second).getEnumerator();
                },
                function () {
                    if (count == index && secondEnumerator.moveNext()) {
                        isEnumerated = true;
                        return this.yieldReturn(secondEnumerator.current());
                    }
                    if (firstEnumerator.moveNext()) {
                        count++;
                        return this.yieldReturn(firstEnumerator.current());
                    }
                    if (!isEnumerated && secondEnumerator.moveNext()) {
                        return this.yieldReturn(secondEnumerator.current());
                    }
                    return false;
                },
                function () {
                    try {
                        Utils.dispose(firstEnumerator);
                    } finally {
                        Utils.dispose(secondEnumerator);
                    }
                }
            );
        });
    };

    Enumerable.prototype.alternate = function (alternateValueOrSequence) {
        var source = this;

        return new Enumerable(function () {
            var buffer;
            var enumerator;
            var alternateSequence;
            var alternateEnumerator;

            return new IEnumerator(
                function () {
                    if (
                        alternateValueOrSequence instanceof Array ||
                        alternateValueOrSequence.getEnumerator != null
                    ) {
                        alternateSequence = Enumerable.from(
                            Enumerable.from(alternateValueOrSequence).toArray()
                        ); // freeze
                    } else {
                        alternateSequence = Enumerable.make(alternateValueOrSequence);
                    }
                    enumerator = source.getEnumerator();
                    if (enumerator.moveNext()) buffer = enumerator.current();
                },
                function () {
                    while (true) {
                        if (alternateEnumerator != null) {
                            if (alternateEnumerator.moveNext()) {
                                return this.yieldReturn(alternateEnumerator.current());
                            } else {
                                alternateEnumerator = null;
                            }
                        }

                        if (buffer == null && enumerator.moveNext()) {
                            buffer = enumerator.current(); // hasNext
                            alternateEnumerator = alternateSequence.getEnumerator();
                            continue; // GOTO
                        } else if (buffer != null) {
                            const retVal = buffer;
                            buffer = null;
                            return this.yieldReturn(retVal);
                        }

                        return this.yieldBreak();
                    }
                },
                function () {
                    try {
                        Utils.dispose(enumerator);
                    } finally {
                        Utils.dispose(alternateEnumerator);
                    }
                }
            );
        });
    };

    // Overload:function(value)
    // Overload:function(value, compareSelector)
    Enumerable.prototype.contains = function (value, compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);
        var enumerator = this.getEnumerator();
        try {
            while (enumerator.moveNext()) {
                if (compareSelector(enumerator.current()) === value) return true;
            }
            return false;
        } finally {
            Utils.dispose(enumerator);
        }
    };

    Enumerable.prototype.defaultIfEmpty = function (defaultValue) {
        var source = this;
        if (defaultValue === undefined) defaultValue = null;

        return new Enumerable(function () {
            var enumerator;
            var isFirst = true;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    if (enumerator.moveNext()) {
                        isFirst = false;
                        return this.yieldReturn(enumerator.current());
                    } else if (isFirst) {
                        isFirst = false;
                        return this.yieldReturn(defaultValue);
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function()
    // Overload:function(compareSelector)
    Enumerable.prototype.distinct = function (compareSelector) {
        return this.except(Enumerable.empty(), compareSelector);
    };

    Enumerable.prototype.distinctUntilChanged = function (compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var compareKey;
            var initial;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (enumerator.moveNext()) {
                        const key = compareSelector(enumerator.current());

                        if (initial) {
                            initial = false;
                            compareKey = key;
                            return this.yieldReturn(enumerator.current());
                        }

                        if (compareKey === key) {
                            continue;
                        }

                        compareKey = key;
                        return this.yieldReturn(enumerator.current());
                    }
                    return this.yieldBreak();
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(second)
    // Overload:function(second, compareSelector)
    Enumerable.prototype.except = function (second, compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var keys;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                    keys = new Dictionary(compareSelector);
                    Enumerable.from(second).forEach(function (key) {
                        keys.add(key);
                    });
                },
                function () {
                    while (enumerator.moveNext()) {
                        const current = enumerator.current();
                        if (!keys.contains(current)) {
                            keys.add(current);
                            return this.yieldReturn(current);
                        }
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(second)
    // Overload:function(second, compareSelector)
    Enumerable.prototype.intersect = function (second, compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var keys;
            var outs;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();

                    keys = new Dictionary(compareSelector);
                    Enumerable.from(second).forEach(function (key) {
                        keys.add(key);
                    });
                    outs = new Dictionary(compareSelector);
                },
                function () {
                    while (enumerator.moveNext()) {
                        const current = enumerator.current();
                        if (!outs.contains(current) && keys.contains(current)) {
                            outs.add(current);
                            return this.yieldReturn(current);
                        }
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(second)
    // Overload:function(second, compareSelector)
    Enumerable.prototype.sequenceEqual = function (second, compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);

        var firstEnumerator = this.getEnumerator();
        try {
            const secondEnumerator = Enumerable.from(second).getEnumerator();
            try {
                while (firstEnumerator.moveNext()) {
                    if (
                        !secondEnumerator.moveNext() ||
                        compareSelector(firstEnumerator.current()) !==
                            compareSelector(secondEnumerator.current())
                    ) {
                        return false;
                    }
                }

                if (secondEnumerator.moveNext()) return false;
                return true;
            } finally {
                Utils.dispose(secondEnumerator);
            }
        } finally {
            Utils.dispose(firstEnumerator);
        }
    };

    Enumerable.prototype.union = function (second, compareSelector) {
        compareSelector = Utils.createLambda(compareSelector);
        var source = this;

        return new Enumerable(function () {
            var firstEnumerator;
            var secondEnumerator;
            var keys;

            return new IEnumerator(
                function () {
                    firstEnumerator = source.getEnumerator();
                    keys = new Dictionary(compareSelector);
                },
                function () {
                    var current;
                    if (secondEnumerator === undefined) {
                        while (firstEnumerator.moveNext()) {
                            current = firstEnumerator.current();
                            if (!keys.contains(current)) {
                                keys.add(current);
                                return this.yieldReturn(current);
                            }
                        }
                        secondEnumerator = Enumerable.from(second).getEnumerator();
                    }
                    while (secondEnumerator.moveNext()) {
                        current = secondEnumerator.current();
                        if (!keys.contains(current)) {
                            keys.add(current);
                            return this.yieldReturn(current);
                        }
                    }
                    return false;
                },
                function () {
                    try {
                        Utils.dispose(firstEnumerator);
                    } finally {
                        Utils.dispose(secondEnumerator);
                    }
                }
            );
        });
    };

    ////////////////////
    // Ordering Methods

    Enumerable.prototype.orderBy = function (keySelector, comparer) {
        return new OrderedEnumerable(this, keySelector, comparer, false);
    };

    Enumerable.prototype.orderByDescending = function (keySelector, comparer) {
        return new OrderedEnumerable(this, keySelector, comparer, true);
    };

    Enumerable.prototype.reverse = function () {
        var source = this;

        return new Enumerable(function () {
            var buffer;
            var index;

            return new IEnumerator(
                function () {
                    buffer = source.toArray();
                    index = buffer.length;
                },
                function () {
                    return index > 0 ? this.yieldReturn(buffer[--index]) : false;
                },
                Functions.Blank
            );
        });
    };

    Enumerable.prototype.shuffle = function () {
        var source = this;

        return new Enumerable(function () {
            var buffer;

            return new IEnumerator(
                function () {
                    buffer = source.toArray();
                },
                function () {
                    if (buffer.length > 0) {
                        const i = Math.floor(Math.random() * buffer.length);
                        return this.yieldReturn(buffer.splice(i, 1)[0]);
                    }
                    return false;
                },
                Functions.Blank
            );
        });
    };

    Enumerable.prototype.weightedSample = function (weightSelector) {
        weightSelector = Utils.createLambda(weightSelector);
        var source = this;

        return new Enumerable(function () {
            var sortedByBound;
            var totalWeight = 0;

            return new IEnumerator(
                function () {
                    sortedByBound = source
                        .choose(function (x) {
                            var weight = weightSelector(x);
                            if (weight <= 0) return null; // ignore 0

                            totalWeight += weight;
                            return { value: x, bound: totalWeight };
                        })
                        .toArray();
                },
                function () {
                    if (sortedByBound.length > 0) {
                        const draw = Math.floor(Math.random() * totalWeight) + 1;

                        var lower = -1;
                        var upper = sortedByBound.length;
                        while (upper - lower > 1) {
                            const index = Math.floor((lower + upper) / 2);
                            if (sortedByBound[index].bound >= draw) {
                                upper = index;
                            } else {
                                lower = index;
                            }
                        }

                        return this.yieldReturn(sortedByBound[upper].value);
                    }

                    return this.yieldBreak();
                },
                Functions.Blank
            );
        });
    };

    ////////////////////
    // Grouping Methods

    // Overload:function(keySelector)
    // Overload:function(keySelector,elementSelector)
    // Overload:function(keySelector,elementSelector,resultSelector)
    // Overload:function(keySelector,elementSelector,resultSelector,compareSelector)
    Enumerable.prototype.groupBy = function (
        keySelector,
        elementSelector,
        resultSelector,
        compareSelector
    ) {
        var source = this;
        keySelector = Utils.createLambda(keySelector);
        elementSelector = Utils.createLambda(elementSelector);
        if (resultSelector != null) resultSelector = Utils.createLambda(resultSelector);
        compareSelector = Utils.createLambda(compareSelector);

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source
                        .toLookup(keySelector, elementSelector, compareSelector)
                        .toEnumerable()
                        .getEnumerator();
                },
                function () {
                    while (enumerator.moveNext()) {
                        return resultSelector == null
                            ? this.yieldReturn(enumerator.current())
                            : this.yieldReturn(
                                  resultSelector(
                                      enumerator.current().key(),
                                      enumerator.current()
                                  )
                              );
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(keySelector)
    // Overload:function(keySelector,elementSelector)
    // Overload:function(keySelector,elementSelector,resultSelector)
    // Overload:function(keySelector,elementSelector,resultSelector,compareSelector)
    Enumerable.prototype.partitionBy = function (
        keySelector,
        elementSelector,
        resultSelector,
        compareSelector
    ) {
        var source = this;
        keySelector = Utils.createLambda(keySelector);
        elementSelector = Utils.createLambda(elementSelector);
        compareSelector = Utils.createLambda(compareSelector);
        var hasResultSelector;
        if (resultSelector == null) {
            hasResultSelector = false;
            resultSelector = function (key, group) {
                return new Grouping(key, group);
            };
        } else {
            hasResultSelector = true;
            resultSelector = Utils.createLambda(resultSelector);
        }

        return new Enumerable(function () {
            var enumerator;
            var key;
            var compareKey;
            var group = [];

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                    if (enumerator.moveNext()) {
                        key = keySelector(enumerator.current());
                        compareKey = compareSelector(key);
                        group.push(elementSelector(enumerator.current()));
                    }
                },
                function () {
                    var hasNext;
                    while ((hasNext = enumerator.moveNext()) == true) {
                        if (
                            compareKey ===
                            compareSelector(keySelector(enumerator.current()))
                        ) {
                            group.push(elementSelector(enumerator.current()));
                        } else break;
                    }

                    if (group.length > 0) {
                        const result = hasResultSelector
                            ? resultSelector(key, Enumerable.from(group))
                            : resultSelector(key, group);
                        if (hasNext) {
                            key = keySelector(enumerator.current());
                            compareKey = compareSelector(key);
                            group = [elementSelector(enumerator.current())];
                        } else group = [];

                        return this.yieldReturn(result);
                    }

                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.buffer = function (count) {
        var source = this;

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    var array = [];
                    var index = 0;
                    while (enumerator.moveNext()) {
                        array.push(enumerator.current());
                        if (++index >= count) return this.yieldReturn(array);
                    }
                    if (array.length > 0) return this.yieldReturn(array);
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    /////////////////////
    // Aggregate Methods

    // Overload:function(func)
    // Overload:function(seed,func)
    // Overload:function(seed,func,resultSelector)
    Enumerable.prototype.aggregate = function (seed, func, resultSelector) {
        resultSelector = Utils.createLambda(resultSelector);
        return resultSelector(this.scan(seed, func, resultSelector).last());
    };

    // Overload:function()
    // Overload:function(selector)
    Enumerable.prototype.average = function (selector) {
        selector = Utils.createLambda(selector);

        var sum = 0;
        var count = 0;
        this.forEach(function (x) {
            sum += selector(x);
            ++count;
        });

        return sum / count;
    };

    // Overload:function()
    // Overload:function(predicate)
    Enumerable.prototype.count = function (predicate) {
        predicate = predicate == null ? Functions.True : Utils.createLambda(predicate);

        var count = 0;
        this.forEach(function (x, i) {
            if (predicate(x, i)) ++count;
        });
        return count;
    };

    // Overload:function()
    // Overload:function(selector)
    Enumerable.prototype.max = function (selector) {
        if (selector == null) selector = Functions.Identity;
        return this.select(selector).aggregate(function (a, b) {
            return a > b ? a : b;
        });
    };

    // Overload:function()
    // Overload:function(selector)
    Enumerable.prototype.min = function (selector) {
        if (selector == null) selector = Functions.Identity;
        return this.select(selector).aggregate(function (a, b) {
            return a < b ? a : b;
        });
    };

    Enumerable.prototype.maxBy = function (keySelector) {
        keySelector = Utils.createLambda(keySelector);
        return this.aggregate(function (a, b) {
            return keySelector(a) > keySelector(b) ? a : b;
        });
    };

    Enumerable.prototype.minBy = function (keySelector) {
        keySelector = Utils.createLambda(keySelector);
        return this.aggregate(function (a, b) {
            return keySelector(a) < keySelector(b) ? a : b;
        });
    };

    // Overload:function()
    // Overload:function(selector)
    Enumerable.prototype.sum = function (selector) {
        if (selector == null) selector = Functions.Identity;
        return this.select(selector).aggregate(0, function (a, b) {
            return a + b;
        });
    };

    //////////////////
    // Paging Methods

    Enumerable.prototype.elementAt = function (index) {
        var value;
        var found = false;
        this.forEach(function (x, i) {
            if (i == index) {
                value = x;
                found = true;
                return false;
            }
        });

        if (!found)
            throw new Error(
                'index is less than 0 or greater than or equal to the number of elements in source.'
            );
        return value;
    };

    Enumerable.prototype.elementAtOrDefault = function (index, defaultValue) {
        if (defaultValue === undefined) defaultValue = null;
        var value;
        var found = false;
        this.forEach(function (x, i) {
            if (i == index) {
                value = x;
                found = true;
                return false;
            }
        });

        return !found ? defaultValue : value;
    };

    // Overload:function()
    // Overload:function(predicate)
    Enumerable.prototype.first = function (predicate) {
        if (predicate != null) return this.where(predicate).first();

        var value;
        var found = false;
        this.forEach(function (x) {
            value = x;
            found = true;
            return false;
        });

        if (!found) throw new Error('first:No element satisfies the condition.');
        return value;
    };

    Enumerable.prototype.firstOrDefault = function (predicate, defaultValue) {
        if (predicate !== undefined) {
            if (
                typeof predicate === Types.Function ||
                typeof Utils.createLambda(predicate) === Types.Function
            ) {
                return this.where(predicate).firstOrDefault(undefined, defaultValue);
            }
            defaultValue = predicate;
        }

        var value;
        var found = false;
        this.forEach(function (x) {
            value = x;
            found = true;
            return false;
        });
        return !found ? defaultValue : value;
    };

    // Overload:function()
    // Overload:function(predicate)
    Enumerable.prototype.last = function (predicate) {
        if (predicate != null) return this.where(predicate).last();

        var value;
        var found = false;
        this.forEach(function (x) {
            found = true;
            value = x;
        });

        if (!found) throw new Error('last:No element satisfies the condition.');
        return value;
    };

    Enumerable.prototype.lastOrDefault = function (predicate, defaultValue) {
        if (predicate !== undefined) {
            if (
                typeof predicate === Types.Function ||
                typeof Utils.createLambda(predicate) === Types.Function
            ) {
                return this.where(predicate).lastOrDefault(undefined, defaultValue);
            }
            defaultValue = predicate;
        }

        var value;
        var found = false;
        this.forEach(function (x) {
            found = true;
            value = x;
        });
        return !found ? defaultValue : value;
    };

    // Overload:function()
    // Overload:function(predicate)
    Enumerable.prototype.single = function (predicate) {
        if (predicate != null) return this.where(predicate).single();

        var value;
        var found = false;
        this.forEach(function (x) {
            if (!found) {
                found = true;
                value = x;
            } else throw new Error('single:sequence contains more than one element.');
        });

        if (!found) throw new Error('single:No element satisfies the condition.');
        return value;
    };

    // Overload:function(defaultValue)
    // Overload:function(defaultValue,predicate)
    Enumerable.prototype.singleOrDefault = function (predicate, defaultValue) {
        if (defaultValue === undefined) defaultValue = null;
        if (predicate != null)
            return this.where(predicate).singleOrDefault(null, defaultValue);

        var value;
        var found = false;
        this.forEach(function (x) {
            if (!found) {
                found = true;
                value = x;
            } else throw new Error('single:sequence contains more than one element.');
        });

        return !found ? defaultValue : value;
    };

    Enumerable.prototype.skip = function (count) {
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                    while (index++ < count && enumerator.moveNext()) {}
                },
                function () {
                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(predicate<element>)
    // Overload:function(predicate<element,index>)
    Enumerable.prototype.skipWhile = function (predicate) {
        predicate = Utils.createLambda(predicate);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;
            var isSkipEnd = false;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (!isSkipEnd) {
                        if (enumerator.moveNext()) {
                            if (!predicate(enumerator.current(), index++)) {
                                isSkipEnd = true;
                                return this.yieldReturn(enumerator.current());
                            }
                            continue;
                        } else return false;
                    }

                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.take = function (count) {
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    return index++ < count && enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(predicate<element>)
    // Overload:function(predicate<element,index>)
    Enumerable.prototype.takeWhile = function (predicate) {
        predicate = Utils.createLambda(predicate);
        var source = this;

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    return enumerator.moveNext() &&
                        predicate(enumerator.current(), index++)
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function()
    // Overload:function(count)
    Enumerable.prototype.takeExceptLast = function (count) {
        if (count == null) count = 1;
        var source = this;

        return new Enumerable(function () {
            if (count <= 0) return source.getEnumerator(); // do nothing

            var enumerator;
            var q = [];

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    while (enumerator.moveNext()) {
                        if (q.length == count) {
                            q.push(enumerator.current());
                            return this.yieldReturn(q.shift());
                        }
                        q.push(enumerator.current());
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.takeFromLast = function (count) {
        if (count <= 0 || count == null) return Enumerable.empty();
        var source = this;

        return new Enumerable(function () {
            var sourceEnumerator;
            var enumerator;
            var q = [];

            return new IEnumerator(
                function () {
                    sourceEnumerator = source.getEnumerator();
                },
                function () {
                    while (sourceEnumerator.moveNext()) {
                        if (q.length == count) q.shift();
                        q.push(sourceEnumerator.current());
                    }
                    if (enumerator == null) {
                        enumerator = Enumerable.from(q).getEnumerator();
                    }
                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(item)
    // Overload:function(predicate)
    Enumerable.prototype.indexOf = function (item) {
        var found = null;

        // item as predicate
        if (typeof item === Types.Function) {
            this.forEach(function (x, i) {
                if (item(x, i)) {
                    found = i;
                    return false;
                }
            });
        } else {
            this.forEach(function (x, i) {
                if (x === item) {
                    found = i;
                    return false;
                }
            });
        }

        return found !== null ? found : -1;
    };

    // Overload:function(item)
    // Overload:function(predicate)
    Enumerable.prototype.lastIndexOf = function (item) {
        var result = -1;

        // item as predicate
        if (typeof item === Types.Function) {
            this.forEach(function (x, i) {
                if (item(x, i)) result = i;
            });
        } else {
            this.forEach(function (x, i) {
                if (x === item) result = i;
            });
        }

        return result;
    };

    ///////////////////
    // Convert Methods

    Enumerable.prototype.cast = function () {
        return this;
    };

    Enumerable.prototype.asEnumerable = function () {
        return Enumerable.from(this);
    };

    Enumerable.prototype.toArray = function () {
        var array = [];
        this.forEach(function (x) {
            array.push(x);
        });
        return array;
    };

    // Overload:function(keySelector)
    // Overload:function(keySelector, elementSelector)
    // Overload:function(keySelector, elementSelector, compareSelector)
    Enumerable.prototype.toLookup = function (
        keySelector,
        elementSelector,
        compareSelector
    ) {
        keySelector = Utils.createLambda(keySelector);
        elementSelector = Utils.createLambda(elementSelector);
        compareSelector = Utils.createLambda(compareSelector);

        var dict = new Dictionary(compareSelector);
        this.forEach(function (x) {
            var key = keySelector(x);
            var element = elementSelector(x);

            var array = dict.get(key);
            if (array !== undefined) array.push(element);
            else dict.add(key, [element]);
        });
        return new Lookup(dict);
    };

    Enumerable.prototype.toObject = function (keySelector, elementSelector) {
        keySelector = Utils.createLambda(keySelector);
        elementSelector = Utils.createLambda(elementSelector);

        var obj = {};
        this.forEach(function (x) {
            obj[keySelector(x)] = elementSelector(x);
        });
        return obj;
    };

    // Overload:function(keySelector, elementSelector)
    // Overload:function(keySelector, elementSelector, compareSelector)
    Enumerable.prototype.toDictionary = function (
        keySelector,
        elementSelector,
        compareSelector
    ) {
        keySelector = Utils.createLambda(keySelector);
        elementSelector = Utils.createLambda(elementSelector);
        compareSelector = Utils.createLambda(compareSelector);

        var dict = new Dictionary(compareSelector);
        this.forEach(function (x) {
            dict.add(keySelector(x), elementSelector(x));
        });
        return dict;
    };

    // Overload:function()
    // Overload:function(replacer)
    // Overload:function(replacer, space)
    Enumerable.prototype.toJSONString = function (replacer, space) {
        if (typeof JSON === Types.Undefined || JSON.stringify == null) {
            throw new Error(
                "toJSONString can't find JSON.stringify. This works native JSON support Browser or include json2.js"
            );
        }
        return JSON.stringify(this.toArray(), replacer, space);
    };

    // Overload:function()
    // Overload:function(separator)
    // Overload:function(separator,selector)
    Enumerable.prototype.toJoinedString = function (separator, selector) {
        if (separator == null) separator = '';
        if (selector == null) selector = Functions.Identity;

        return this.select(selector).toArray().join(separator);
    };

    //////////////////
    // Action Methods

    // Overload:function(action<element>)
    // Overload:function(action<element,index>)
    Enumerable.prototype.doAction = function (action) {
        var source = this;
        action = Utils.createLambda(action);

        return new Enumerable(function () {
            var enumerator;
            var index = 0;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    if (enumerator.moveNext()) {
                        action(enumerator.current(), index++);
                        return this.yieldReturn(enumerator.current());
                    }
                    return false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    // Overload:function(action<element>)
    // Overload:function(action<element,index>)
    // Overload:function(func<element,bool>)
    // Overload:function(func<element,index,bool>)
    Enumerable.prototype.forEach = function (action) {
        action = Utils.createLambda(action);

        var index = 0;
        var enumerator = this.getEnumerator();
        try {
            while (enumerator.moveNext()) {
                if (action(enumerator.current(), index++) === false) break;
            }
        } finally {
            Utils.dispose(enumerator);
        }
    };

    Enumerable.prototype.force = function () {
        var enumerator = this.getEnumerator();

        try {
            while (enumerator.moveNext()) {}
        } finally {
            Utils.dispose(enumerator);
        }
    };

    //////////////////////
    // Functional Methods

    Enumerable.prototype.letBind = function (func) {
        func = Utils.createLambda(func);
        var source = this;

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = Enumerable.from(func(source)).getEnumerator();
                },
                function () {
                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.share = function () {
        var source = this;
        var sharedEnumerator;
        var disposed = false;

        return new DisposableEnumerable(
            function () {
                return new IEnumerator(
                    function () {
                        if (sharedEnumerator == null) {
                            sharedEnumerator = source.getEnumerator();
                        }
                    },
                    function () {
                        if (disposed) throw new Error('enumerator is disposed');

                        return sharedEnumerator.moveNext()
                            ? this.yieldReturn(sharedEnumerator.current())
                            : false;
                    },
                    Functions.Blank
                );
            },
            function () {
                disposed = true;
                Utils.dispose(sharedEnumerator);
            }
        );
    };

    Enumerable.prototype.memoize = function () {
        var source = this;
        var cache;
        var enumerator;
        var disposed = false;

        return new DisposableEnumerable(
            function () {
                var index = -1;

                return new IEnumerator(
                    function () {
                        if (enumerator == null) {
                            enumerator = source.getEnumerator();
                            cache = [];
                        }
                    },
                    function () {
                        if (disposed) throw new Error('enumerator is disposed');

                        index++;
                        if (cache.length <= index) {
                            return enumerator.moveNext()
                                ? this.yieldReturn((cache[index] = enumerator.current()))
                                : false;
                        }

                        return this.yieldReturn(cache[index]);
                    },
                    Functions.Blank
                );
            },
            function () {
                disposed = true;
                Utils.dispose(enumerator);
                cache = null;
            }
        );
    };

    // Iterator support (ES6 for..of)
    if (Utils.hasNativeIteratorSupport()) {
        Enumerable.prototype[Symbol.iterator] = function () {
            return {
                enumerator: this.getEnumerator(),
                next: function () {
                    if (this.enumerator.moveNext()) {
                        return {
                            done: false,
                            value: this.enumerator.current()
                        };
                    } else {
                        return { done: true };
                    }
                }
            };
        };
    }

    //////////////////////////
    // Error Handling Methods

    Enumerable.prototype.catchError = function (handler) {
        handler = Utils.createLambda(handler);
        var source = this;

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    try {
                        return enumerator.moveNext()
                            ? this.yieldReturn(enumerator.current())
                            : false;
                    } catch (e) {
                        handler(e);
                        return false;
                    }
                },
                function () {
                    Utils.dispose(enumerator);
                }
            );
        });
    };

    Enumerable.prototype.finallyAction = function (finallyAction) {
        finallyAction = Utils.createLambda(finallyAction);
        var source = this;

        return new Enumerable(function () {
            var enumerator;

            return new IEnumerator(
                function () {
                    enumerator = source.getEnumerator();
                },
                function () {
                    return enumerator.moveNext()
                        ? this.yieldReturn(enumerator.current())
                        : false;
                },
                function () {
                    try {
                        Utils.dispose(enumerator);
                    } finally {
                        finallyAction();
                    }
                }
            );
        });
    };

    /////////////////
    // Debug Methods

    // Overload:function()
    // Overload:function(selector)
    Enumerable.prototype.log = function (selector) {
        selector = Utils.createLambda(selector);

        return this.doAction(function (item) {
            if (typeof console !== Types.Undefined) {
                console.log(selector(item));
            } else if (isFlash) {
                fl.trace(selector(item));
            }
        });
    };

    // Overload:function()
    // Overload:function(message)
    // Overload:function(message,selector)
    Enumerable.prototype.trace = function (message, selector) {
        if (message == null) message = 'Trace';
        selector = Utils.createLambda(selector);

        return this.doAction(function (item) {
            if (typeof console !== Types.Undefined) {
                console.log(message, selector(item));
            } else if (isFlash) {
                fl.trace(selector(item));
            }
        });
    };

    ///////////
    // Private

    var OrderedEnumerable = function (source, keySelector, comparer, descending, parent) {
        this.source = source;
        this.keySelector = Utils.createLambda(keySelector);
        this.descending = descending;
        this.parent = parent;

        if (comparer) this.comparer = Utils.createLambda(comparer);
    };
    OrderedEnumerable.prototype = new Enumerable();

    OrderedEnumerable.prototype.createOrderedEnumerable = function (
        keySelector,
        comparer,
        descending
    ) {
        return new OrderedEnumerable(
            this.source,
            keySelector,
            comparer,
            descending,
            this
        );
    };

    OrderedEnumerable.prototype.thenBy = function (keySelector, comparer) {
        return this.createOrderedEnumerable(keySelector, comparer, false);
    };

    OrderedEnumerable.prototype.thenByDescending = function (keySelector, comparer) {
        return this.createOrderedEnumerable(keySelector, comparer, true);
    };

    OrderedEnumerable.prototype.getEnumerator = function () {
        var self = this;
        var buffer;
        var indexes;
        var index = 0;

        return new IEnumerator(
            function () {
                buffer = [];
                indexes = [];
                self.source.forEach(function (item, index) {
                    buffer.push(item);
                    indexes.push(index);
                });
                var sortContext = SortContext.create(self, null);
                sortContext.GenerateKeys(buffer);

                indexes.sort(function (a, b) {
                    return sortContext.compare(a, b);
                });
            },
            function () {
                return index < indexes.length
                    ? this.yieldReturn(buffer[indexes[index++]])
                    : false;
            },
            Functions.Blank
        );
    };

    var SortContext = function (keySelector, comparer, descending, child) {
        this.keySelector = keySelector;
        this.descending = descending;
        this.child = child;
        this.comparer = comparer;
        this.keys = null;
    };

    SortContext.create = function (orderedEnumerable, currentContext) {
        var context = new SortContext(
            orderedEnumerable.keySelector,
            orderedEnumerable.comparer,
            orderedEnumerable.descending,
            currentContext
        );

        if (orderedEnumerable.parent != null)
            return SortContext.create(orderedEnumerable.parent, context);
        return context;
    };

    SortContext.prototype.GenerateKeys = function (source) {
        var len = source.length;
        var keySelector = this.keySelector;
        var keys = new Array(len);
        for (var i = 0; i < len; i++) keys[i] = keySelector(source[i]);
        this.keys = keys;

        if (this.child != null) this.child.GenerateKeys(source);
    };

    SortContext.prototype.compare = function (index1, index2) {
        var comparison = this.comparer
            ? this.comparer(this.keys[index1], this.keys[index2])
            : Utils.compare(this.keys[index1], this.keys[index2]);

        if (comparison == 0) {
            if (this.child != null) return this.child.compare(index1, index2);
            return Utils.compare(index1, index2);
        }

        return this.descending ? -comparison : comparison;
    };

    var DisposableEnumerable = function (getEnumerator, dispose) {
        this.dispose = dispose;
        Enumerable.call(this, getEnumerator);
    };
    DisposableEnumerable.prototype = new Enumerable();

    var ArrayEnumerable = function (source) {
        this.getSource = function () {
            return source;
        };
    };
    ArrayEnumerable.prototype = new Enumerable();

    ArrayEnumerable.prototype.any = function (predicate) {
        return predicate == null
            ? this.getSource().length > 0
            : Enumerable.prototype.any.apply(this, arguments);
    };

    ArrayEnumerable.prototype.count = function (predicate) {
        return predicate == null
            ? this.getSource().length
            : Enumerable.prototype.count.apply(this, arguments);
    };

    ArrayEnumerable.prototype.elementAt = function (index) {
        var source = this.getSource();
        return 0 <= index && index < source.length
            ? source[index]
            : Enumerable.prototype.elementAt.apply(this, arguments);
    };

    ArrayEnumerable.prototype.elementAtOrDefault = function (index, defaultValue) {
        if (defaultValue === undefined) defaultValue = null;
        var source = this.getSource();
        return 0 <= index && index < source.length ? source[index] : defaultValue;
    };

    ArrayEnumerable.prototype.first = function (predicate) {
        var source = this.getSource();
        return predicate == null && source.length > 0
            ? source[0]
            : Enumerable.prototype.first.apply(this, arguments);
    };

    ArrayEnumerable.prototype.firstOrDefault = function (predicate, defaultValue) {
        if (predicate !== undefined) {
            return Enumerable.prototype.firstOrDefault.apply(this, arguments);
        }
        defaultValue = predicate;

        var source = this.getSource();
        return source.length > 0 ? source[0] : defaultValue;
    };

    ArrayEnumerable.prototype.last = function (predicate) {
        var source = this.getSource();
        return predicate == null && source.length > 0
            ? source[source.length - 1]
            : Enumerable.prototype.last.apply(this, arguments);
    };

    ArrayEnumerable.prototype.lastOrDefault = function (predicate, defaultValue) {
        if (predicate !== undefined) {
            return Enumerable.prototype.lastOrDefault.apply(this, arguments);
        }
        defaultValue = predicate;

        var source = this.getSource();
        return source.length > 0 ? source[source.length - 1] : defaultValue;
    };

    ArrayEnumerable.prototype.skip = function (count) {
        var source = this.getSource();

        return new Enumerable(function () {
            var index;

            return new IEnumerator(
                function () {
                    index = count < 0 ? 0 : count;
                },
                function () {
                    return index < source.length
                        ? this.yieldReturn(source[index++])
                        : false;
                },
                Functions.Blank
            );
        });
    };

    ArrayEnumerable.prototype.takeExceptLast = function (count) {
        if (count == null) count = 1;
        return this.take(this.getSource().length - count);
    };

    ArrayEnumerable.prototype.takeFromLast = function (count) {
        return this.skip(this.getSource().length - count);
    };

    ArrayEnumerable.prototype.reverse = function () {
        var source = this.getSource();

        return new Enumerable(function () {
            var index;

            return new IEnumerator(
                function () {
                    index = source.length;
                },
                function () {
                    return index > 0 ? this.yieldReturn(source[--index]) : false;
                },
                Functions.Blank
            );
        });
    };

    ArrayEnumerable.prototype.sequenceEqual = function (second, compareSelector) {
        if (
            (second instanceof ArrayEnumerable || second instanceof Array) &&
            compareSelector == null &&
            Enumerable.from(second).count() != this.count()
        ) {
            return false;
        }

        return Enumerable.prototype.sequenceEqual.apply(this, arguments);
    };

    ArrayEnumerable.prototype.toJoinedString = function (separator, selector) {
        var source = this.getSource();
        if (selector != null || !(source instanceof Array)) {
            return Enumerable.prototype.toJoinedString.apply(this, arguments);
        }

        if (separator == null) separator = '';
        return source.join(separator);
    };

    ArrayEnumerable.prototype.getEnumerator = function () {
        var source = this.getSource();
        var index = -1;

        // fast and simple enumerator
        return {
            current: function () {
                return source[index];
            },
            moveNext: function () {
                return ++index < source.length;
            },
            dispose: Functions.Blank
        };
    };

    // optimization for multiple where and multiple select and whereselect

    var WhereEnumerable = function (source, predicate) {
        this.prevSource = source;
        this.prevPredicate = predicate; // predicate.length always <= 1
    };
    WhereEnumerable.prototype = new Enumerable();

    WhereEnumerable.prototype.where = function (predicate) {
        predicate = Utils.createLambda(predicate);

        if (predicate.length <= 1) {
            const prevPredicate = this.prevPredicate;
            const composedPredicate = function (x) {
                return prevPredicate(x) && predicate(x);
            };
            return new WhereEnumerable(this.prevSource, composedPredicate);
        } else {
            // if predicate use index, can't compose
            return Enumerable.prototype.where.call(this, predicate);
        }
    };

    WhereEnumerable.prototype.select = function (selector) {
        selector = Utils.createLambda(selector);

        return selector.length <= 1
            ? new WhereSelectEnumerable(this.prevSource, this.prevPredicate, selector)
            : Enumerable.prototype.select.call(this, selector);
    };

    WhereEnumerable.prototype.getEnumerator = function () {
        var predicate = this.prevPredicate;
        var source = this.prevSource;
        var enumerator;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
            },
            function () {
                while (enumerator.moveNext()) {
                    if (predicate(enumerator.current())) {
                        return this.yieldReturn(enumerator.current());
                    }
                }
                return false;
            },
            function () {
                Utils.dispose(enumerator);
            }
        );
    };

    var WhereSelectEnumerable = function (source, predicate, selector) {
        this.prevSource = source;
        this.prevPredicate = predicate; // predicate.length always <= 1 or null
        this.prevSelector = selector; // selector.length always <= 1
    };
    WhereSelectEnumerable.prototype = new Enumerable();

    WhereSelectEnumerable.prototype.where = function (predicate) {
        predicate = Utils.createLambda(predicate);

        return predicate.length <= 1
            ? new WhereEnumerable(this, predicate)
            : Enumerable.prototype.where.call(this, predicate);
    };

    WhereSelectEnumerable.prototype.select = function (selector) {
        selector = Utils.createLambda(selector);

        if (selector.length <= 1) {
            const prevSelector = this.prevSelector;
            const composedSelector = function (x) {
                return selector(prevSelector(x));
            };
            return new WhereSelectEnumerable(
                this.prevSource,
                this.prevPredicate,
                composedSelector
            );
        } else {
            // if selector uses index, can't compose
            return Enumerable.prototype.select.call(this, selector);
        }
    };

    WhereSelectEnumerable.prototype.getEnumerator = function () {
        var predicate = this.prevPredicate;
        var selector = this.prevSelector;
        var source = this.prevSource;
        var enumerator;

        return new IEnumerator(
            function () {
                enumerator = source.getEnumerator();
            },
            function () {
                while (enumerator.moveNext()) {
                    if (predicate == null || predicate(enumerator.current())) {
                        return this.yieldReturn(selector(enumerator.current()));
                    }
                }
                return false;
            },
            function () {
                Utils.dispose(enumerator);
            }
        );
    };

    ///////////////
    // Collections

    var Dictionary = (function () {
        // static utility methods
        var callHasOwnProperty = function (target, key) {
            return Object.prototype.hasOwnProperty.call(target, key);
        };

        var computeHashCode = function (obj) {
            if (obj === null) return 'null';
            if (obj === undefined) return 'undefined';

            return typeof obj.toString === Types.Function
                ? obj.toString()
                : Object.prototype.toString.call(obj);
        };

        // LinkedList for Dictionary
        var HashEntry = function (key, value) {
            this.key = key;
            this.value = value;
            this.prev = null;
            this.next = null;
        };

        var EntryList = function () {
            this.first = null;
            this.last = null;
        };
        EntryList.prototype = {
            addLast: function (entry) {
                if (this.last != null) {
                    this.last.next = entry;
                    entry.prev = this.last;
                    this.last = entry;
                } else this.first = this.last = entry;
            },

            replace: function (entry, newEntry) {
                if (entry.prev != null) {
                    entry.prev.next = newEntry;
                    newEntry.prev = entry.prev;
                } else this.first = newEntry;

                if (entry.next != null) {
                    entry.next.prev = newEntry;
                    newEntry.next = entry.next;
                } else this.last = newEntry;
            },

            remove: function (entry) {
                if (entry.prev != null) entry.prev.next = entry.next;
                else this.first = entry.next;

                if (entry.next != null) entry.next.prev = entry.prev;
                else this.last = entry.prev;
            }
        };

        // Overload:function()
        // Overload:function(compareSelector)
        var Dictionary = function (compareSelector) {
            this.countField = 0;
            this.entryList = new EntryList();
            this.buckets = {}; // as Dictionary<string,List<object>>
            this.compareSelector =
                compareSelector == null ? Functions.Identity : compareSelector;
        };
        Dictionary.prototype = {
            add: function (key, value) {
                var compareKey = this.compareSelector(key);
                var hash = computeHashCode(compareKey);
                var entry = new HashEntry(key, value);
                if (callHasOwnProperty(this.buckets, hash)) {
                    const array = this.buckets[hash];
                    for (var i = 0; i < array.length; i++) {
                        if (this.compareSelector(array[i].key) === compareKey) {
                            this.entryList.replace(array[i], entry);
                            array[i] = entry;
                            return;
                        }
                    }
                    array.push(entry);
                } else {
                    this.buckets[hash] = [entry];
                }
                this.countField++;
                this.entryList.addLast(entry);
            },

            get: function (key) {
                var compareKey = this.compareSelector(key);
                var hash = computeHashCode(compareKey);
                if (!callHasOwnProperty(this.buckets, hash)) return undefined;

                var array = this.buckets[hash];
                for (var i = 0; i < array.length; i++) {
                    const entry = array[i];
                    if (this.compareSelector(entry.key) === compareKey)
                        return entry.value;
                }
                return undefined;
            },

            set: function (key, value) {
                var compareKey = this.compareSelector(key);
                var hash = computeHashCode(compareKey);
                if (callHasOwnProperty(this.buckets, hash)) {
                    const array = this.buckets[hash];
                    for (var i = 0; i < array.length; i++) {
                        if (this.compareSelector(array[i].key) === compareKey) {
                            const newEntry = new HashEntry(key, value);
                            this.entryList.replace(array[i], newEntry);
                            array[i] = newEntry;
                            return true;
                        }
                    }
                }
                return false;
            },

            contains: function (key) {
                var compareKey = this.compareSelector(key);
                var hash = computeHashCode(compareKey);
                if (!callHasOwnProperty(this.buckets, hash)) return false;

                var array = this.buckets[hash];
                for (var i = 0; i < array.length; i++) {
                    if (this.compareSelector(array[i].key) === compareKey) return true;
                }
                return false;
            },

            clear: function () {
                this.countField = 0;
                this.buckets = {};
                this.entryList = new EntryList();
            },

            remove: function (key) {
                var compareKey = this.compareSelector(key);
                var hash = computeHashCode(compareKey);
                if (!callHasOwnProperty(this.buckets, hash)) return;

                var array = this.buckets[hash];
                for (var i = 0; i < array.length; i++) {
                    if (this.compareSelector(array[i].key) === compareKey) {
                        this.entryList.remove(array[i]);
                        array.splice(i, 1);
                        if (array.length == 0) delete this.buckets[hash];
                        this.countField--;
                        return;
                    }
                }
            },

            count: function () {
                return this.countField;
            },

            toEnumerable: function () {
                var self = this;
                return new Enumerable(function () {
                    var currentEntry;

                    return new IEnumerator(
                        function () {
                            currentEntry = self.entryList.first;
                        },
                        function () {
                            if (currentEntry != null) {
                                const result = {
                                    key: currentEntry.key,
                                    value: currentEntry.value
                                };
                                currentEntry = currentEntry.next;
                                return this.yieldReturn(result);
                            }
                            return false;
                        },
                        Functions.Blank
                    );
                });
            }
        };

        return Dictionary;
    })();

    // dictionary = Dictionary<TKey, TValue[]>
    var Lookup = function (dictionary) {
        this.count = function () {
            return dictionary.count();
        };
        this.get = function (key) {
            return Enumerable.from(dictionary.get(key));
        };
        this.contains = function (key) {
            return dictionary.contains(key);
        };
        this.toEnumerable = function () {
            return dictionary.toEnumerable().select(function (kvp) {
                return new Grouping(kvp.key, kvp.value);
            });
        };
    };

    var Grouping = function (groupKey, elements) {
        this.key = function () {
            return groupKey;
        };
        ArrayEnumerable.call(this, elements);
    };
    Grouping.prototype = new ArrayEnumerable();

    // export default Enumerable;
    return Enumerable;
});
